最近4个月遇到过2次ghdl
程序逆向题,虽然核心代码都很简单,但是感觉还是有必要稍微总结一下经验。
GHDL Reverse
What
VHDL
在数电是学过的,大抵就是一种模拟电子电路的硬件语言。
简单来说,这个GHDL
就是就是在Linux平台上解析VHDL
或者Verilog
脚本,一通转换,翻译以后进入LLVM
或者GCC
进行编译,转换成Linux的ELF可执行文件。
About — GHDL 3.0.0-dev documentation
这里面有GHDL
流程图(不想在我的博客里面搞图片)
安装
https://github.com/ghdl/ghdl/releases
这边给了已经编译过的现成可执行程序。下一个Linux的。同时再下一份源码便于分析。
1 | ghdl-gha-ubuntu-18.04-gcc // Ubuntu 18 |
Windows版本比较特殊,由于某些bug原因暂时没能搞明白,所以不管了。Ubuntu版本的会轻松很多。
例子
hello.vhdl
1 | -- hello.vhdl |
这个语法据我研究好像是Ada编程语言?
编译
GHDL
有三种底层,mcode
,LLVM
和gcc
。这里使用的是gcc
后端。mcode
是内部程式码,似乎是给Windows平台提供的支持方案。
首先,解析hello.vhdl
。这个操作会分析你的vhdl
文件的语法错误,若无误则会生成一份work-obj93.cf
以及hello.o
可重定位文件。
1 | ./ghdl -a hello.vhdl |
然后
1 | ./ghdl -e hello_world |
此时将会基于刚刚解析的结果编译出一份hello_world
可执行程序(在源码中定义过entity hello_world
,所以生成的模块名就得是这个)。
1 | ./ghdl -r hello_world |
则会编译且执行。这样就会打印Hello world!
了。
当然由于已经编译出二进制文件了,所以直接
1 | ./hello_world |
也行。
逆向
直接拖进IDA看。(如果被strip
过了,那么自己整一份有调试信息的,然后Bindiff
即可恢复大部分函数。)
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
1 | __int64 __fastcall ghdl_main(__int64 a1, __int64 *a2) |
进入grt__main__run
函数
1 | __int64 grt__main__run() |
核心在grt_main_elab()
函数
1 | __int64 grt_main_elab() |
其中_ghdl_run_through_longjump
指向了一个grt__main__ghdl_elaborate_wrapper
函数
1 | __int64 grt__main__ghdl_elaborate_wrapper() |
1 | __int64 _ghdl_ELABORATE() |
然后在work__hello_world__ARCH__behaviour__STMT_ELAB
1 | __int64 __fastcall work__hello_world__ARCH__behaviour__STMT_ELAB(__int64 a1) |
_ghdl_process_register
中的work__hello_world__ARCH__behaviour__P0__PROC
就是
1 | __int64 __fastcall work__hello_world__ARCH__behaviour__P0__PROC(__int64 a1) |
就是主程序逻辑了。
不过我个人觉得_ghdl_process_register
这里应该不是立刻执行的,而是初始化寄存器,得到函数指针啥的,然后到后面grt_main__run_simul
这样的函数再去执行。
由于这个例子太简单,并不能展现出核心函数的什么具体特征,但是用于分析程序启动流程倒是可以的。
源码解读
Ghdl_Process_Register
在src/grt/grt-processes.ads
中发现
1 | -- Register a process during elaboration. |
对应了_ghdl_process_register
函数。
第二个参数就是程序地址。
Ghdl_Stack2_
同样在grt-processes.ads
和grt-processes.adb
中有
_ghdl_stack2_mark
和_ghdl_stack2_release
函数的定义。
1 | function Ghdl_Stack2_Mark return Mark_Id |
和IDA中
1 | __int64 _ghdl_stack2_mark() |
一致。
这个函数的作用大致就是分配栈空间的。具体作用不需要知道,只需要了解这个函数存在于一段主核心逻辑的开头和结尾,是一个特征函数。
heartbeat.vhdl
上面的hello.vhdl
证明了ghdl
可以被用作通用编程语言。第二个例子则更接近实际硬件编程。
1 | library ieee; |
这个程序并没有调用write
,writeline
函数,所以没有一般的控制台输出。相反的,它是输出0-1波形的。
保存波形文件,通过
1 | ./ghdl -r heartbeat --wave=wave.ghw |
获取ghw
相关信息:
1 | ./ghwdump [-OPTION] wave.ghw |
不过这道程序由于没法自动退出,手动强制退出可能使得生成的wave.ghw
文件也有问题,会导致ghwdump
出现SIGABRT
异常退出。
还有gtkwave
,在Ubuntu18.04
上面可以直接sudo apt install gtkwave
来安装。不过由于需要图形界面,需要使用VMWare虚拟机或者WSL2+图形界面插件来使用。
逆向
日常操作,很快发现主函数:
1 | __int64 __fastcall work__heartbeat__ARCH__behaviour__clk_process__PROC(__int64 a1) |
_ghdl_process_wait_timeout
在grt-processed.adb
中
1 | -- Return TRUE if woken up by a timeout. |
这个对应vhdl
中的wait for
关键词。
_ghdl_signal_direct_assign
在grt/grt-signals.adb
中
1 | -- Assigning a waveform to a signal: |
这个就是输出信号。
传参结构体逆向
可以发现这个函数在很多地方用了a1
的值。猜测a1
是一个很重要的结构体,但无奈这个函数似乎是编译器生成的,且我不了解Ada编程语言的定义习惯。
不过交叉引用,向上查找能直接查到的库函数中对此a1
的应用,有助于分析。
向上查到这里,发现(A *)_ghdl_malloc0(48LL);
,可以得知此结构体大小为48字节,即6个QWORD。
1 | __int64 _ghdl_ELABORATE() |
field_18=Ghdl_Value_Ptr
(QWORD)
field_10=Ghdl_Signal_Ptr
(QWORD)
1 | function Ghdl_Create_Signal_E8 (Val_Ptr : Ghdl_Value_Ptr; |
1 | v1 = (_BYTE *)vm->field_18; |
1 | __int64 __fastcall work__heartbeat__DECL_ELAB(A *a1) |
1 | procedure Ghdl_Signal_Merge_Rti (Sig : Ghdl_Signal_Ptr; |
field_2C=Value
(BYTE)
1 | A *__fastcall work__heartbeat__ARCH__behaviour__STMT_ELAB(A *a1) |
1 | procedure Ghdl_Signal_Add_Direct_Driver (Sign : Ghdl_Signal_Ptr; |
field_28=System
(DWORD)
1 | A *__fastcall work__heartbeat__ARCH__behaviour__STMT_ELAB(A *a1) |
1 | procedure Ghdl_Process_Register |
field_0=__ARCH__behaviour__RTI
(QWORD*)
field_20=10000000
(QWORD)
1 | A *__fastcall work__heartbeat__ARCH__behaviour__DECL_ELAB(A *a1) |
这个__DECL_ELAB
函数和__STMT_ELAB
函数都在_ghdl_ELABORATE
函数中。
根据交叉引用发现,这个field_20
应该就是源码中定义的clk_period
常量。
可以合理推测,DECL_ELAB
函数是用于初始化常量变量的,而STMT_ELAB
则是存储了执行逻辑
恢复后
1 | 00000000 A struc ; (sizeof=0x30, mappedto_33) |
1 | A *__fastcall work__heartbeat__ARCH__behaviour__clk_process__PROC(A *vm) |
不过那个Ghdl_Signal_Ptr
暂时不管了,大抵指向了一个Signal
结构体,然后又要我去分析Create_Signal
函数,太过麻烦。
adder.vhdl
1 | entity adder is |
数电里面这种玩意学的老会了,就是个全加器的vhdl
描述。
1 | __int64 _ghdl_ELABORATE() |
可以发现不同的程序,传参结构体组成也不一样。得具体问题具体分析。
1 | -- Creating a signal: |
关于RTI
Run Time Information (RTI) — GHDL 3.0.0-dev documentation
anyway,Ghdl_Signal_Name_Rti
的第一个参数是一个Signal
,正是在脚本定义的那几个变量(输出用的)。
1 | port (i0, i1 : in bit; ci : in bit; s : out bit; co : out bit); |
1 | __int64 __fastcall work__adder__DECL_ELAB(_QWORD *a1) |
没啥用。
恢复
草率的恢复了一下结构体
1 | 00000000 A struc ; (sizeof=0x68, mappedto_33) |
其中
1 | Value_Ptr |
对应一个声明的从port
输出的值,一次是i0
,i1
,ci
,s
,co
。
1 | __int64 __fastcall work__adder__ARCH__rtl__STMT_ELAB(A *a1) |
还是能从sensitized_process_register
这样的函数找到主逻辑函数地址。
1 | char __fastcall work__adder__ARCH__rtl__P0__PROC(A *a1) |
这个对应的显然就是
1 | s <= i0 xor i1 xor ci; |
指令
1 | char __fastcall work__adder__ARCH__rtl__P1__PROC(A *a1) |
则是
1 | co <= (i0 and i1) or (i0 and ci) or (i1 and ci); |
所以说,结构体的恢复很重要。
TestBench
这个例子官方文档还有一个有关testbench
编写流程。不讲了,这是数电开发知识。
Full adder module and testbench — GHDL 3.0.0-dev documentation
技巧总结
- 使用
Bindiff
可以恢复很多库函数,极大的帮助逆向过程。 - 学会恢复结构体
- 对应源码找库函数原型
- 有
_process_register
后缀的函数中可能有核心函数地址指针
CTF例题
前面几个的逆向流程其实并不是我一开始打算的东西。可能因为都是这几个官方的例子都是线性流程的,并没有体现CTF题目中的那种想要的结果。
SUS_CTF2022——hello_world
基于ghdl
的Windows程序逆向。
一开始我是通过在程序scanf
的时候中断,然后往上溯源慢慢找到核心代码段的。
经过今天的总结,得知了更快速更轻松的手法。
首先能Bindiff
的最好先整一下,设置可信度0.9,多几个已知库函数能大大加速过程。
_start
函数是Mingw
典型的编译器胶水函数,anyway我们找到main
函数的位置
(以下重要函数皆已重命名,或者是Bindiff
辅助的)
1 | __int64 __fastcall sub_7FF7BAD6A570(unsigned int a1, __int64 *a2) |
1 | __int64 __fastcall ghdl_main(unsigned int a1, __int64 *a2) |
1 | __int64 grt__main__run() |
进入grt_main_elab
1 | __int64 grt_main_elab() |
_ghdl_run_through_longjump(grt__main__ghdl_elaborate_wrapper)
很经典,传入的是个函数地址。
1 | __int64 ghdl_elaborate() |
这个函数直接照着其他上面的例子改就行了。(这个函数是直接对着hello_world
的ELABORATE
函数重命名的)
若是用了port
定义硬件波形输出,那么这个函数应该会有很多的 _ghdl_signal_name_rti
函数被应用到。但是没有,和hello.vhdl
一样,所以是应该就是当成一般通用语言来用了。
v0
就是要恢复的结构体。但是这个题目里面太庞大了,懒得搞。
1 | __int64 __fastcall _ARCH__behaviour__STMT_ELAB(__int64 a1) |
_register
后缀函数,第二个是主核心函数。重命名为MainLoop
1 | __int64 __fastcall MainLoop(vm *vm) |
这一大块大概就是由于有分支的原因,而被ghdl
编译成了switch-case
类的迫真虚拟机形态。
而且每一个case
基本都有很明显的_ghdl_stack2_mark()
函数作为开头。
每一个case
头部下个断点,便可以调试得知每一个块的流程。
注意到case 11
有
1 | v6 = dword_7FF7BAE05B80[v5]; |
case 10
有
1 | tmp = Enc[v21]; |
两个数组,直接异或就是flag
。
由于核心加密过程过于简单,所以找到这两个数组就是赢。
当然如果有人慢慢调试,从输入开始慢慢看流程的话,偶尔可能会发现存在以2,3组成的8比特串。2对应0,3对应1,其实就代表了一个字节。如果是字符的话那就是个ASCII码。这玩意在2次CTF题目中都出现过。(就算不是ASCII码,稍微有点耐心,爆破出每个字符对应的比特串也是可以的)
总结
另一道题上学期做的了,不想找。一个例子足矣
找到核心函数其实是ghdl
逆向中非常重要的一个环节。
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2022/03/04/ghdl reverse/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!